package Domain;

import Persistence.PersistenceController;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.Files;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A connection with a client
 *
 * @author kenneth
 */
public class ClientConnection implements Runnable {

    private Socket link; // connection with the client
    private String email; // client email
    private File dir; // file object of the user his root directory on the server
    
    private static final int BUFFERSIZE = 1024;
    
    private DataInputStream socketIn; // receive from client
    private DataOutputStream socketOut; // send to client
    
    private final Serverlog serverlog; // write to server log
    
    private boolean run;    

    /**
     * Constructor
     * 
     * @param link The connection with the client.
     * @param serverlog The server log, can be used to send messages to the server info area.
     */
    public ClientConnection(Socket link, Serverlog serverlog) {
        this.link = link;
        this.serverlog = serverlog;
        initConnection();
    }
    
    /**
     * Initializes the input- & ouputstreams.
     */
    private void initConnection() {
        try {
            socketIn = new DataInputStream(link.getInputStream());
            socketOut = new DataOutputStream(link.getOutputStream());
        } catch (IOException e) {
            serverlog.addMessage("Opening streams failed");
        }
    }

    /**
     * Closes the connection with the client.
     */
    private void closeConnection() {
        try {
            link.close();
            serverlog.addMessage("Closing " + email + "'s connection");
        } catch (IOException ex) {
            serverlog.addMessage(email + "'s connection was already closed");
        }
    }

    @Override
    public void run() {
        run = true;
        while (run) {
            String state = "";
            try {
                state = socketIn.readUTF();
            } catch (IOException ex) {
                ex.printStackTrace();
                serverlog.addMessage("Connection with client failed");
                run = false;
            }
            if (state.equals("login")) {
                login();
            } else if (state.equals("register")) {
                register();
            } else if (state.equals("resetPassword")) {
                resetPassword();
            } else if (state.equals("acceptFile")) {
                acceptFileFromClient();
            } else if (state.equals("deleteFile")) {
                deleteFileOnServer();
            } else if (state.equals("synchronize")) {
                synchronizeFiles();
            } else if (state.equals("closeConnection")) {
                closeConnection();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                run = false;
            }
        }
    }

    /**
     * Read the email & password from the client.
     * Try to log in (contact database).
     * If log in succeeds checkAndCreateFolder() will be called.
     * Send a boolean to the client to tell whether log in was successful or not.
     */
    private void login() {
        String password = "";
        try {
            email = socketIn.readUTF();
            password = socketIn.readUTF();
        } catch (IOException ex) {
            ex.printStackTrace();
            serverlog.addMessage("Connection with client failed");
        }
        boolean loginSuccess = PersistenceController.getInstance().authenticateUser(email, password);
        if (loginSuccess) {
            serverlog.addMessage("User " + email + " has authenticated");
            checkAndCreateFolder();
        } else {
            serverlog.addMessage("User " + email + " has failed to authenticate");
        }
        sendResult(loginSuccess);
    }

    /**
     * Read the email & password from the client.
     * Try to register an account for the email address (contact database).
     * Send a boolean to the client to tell whether registration was successful or not.
     */
    private void register() {
        String password = "";
        try {
            email = socketIn.readUTF();
            password = socketIn.readUTF();
        } catch (IOException ex) {
            serverlog.addMessage("Connection with client failed");
        }
        boolean registerSuccess = PersistenceController.getInstance().registerUser(email, password);
        if (registerSuccess) {
            serverlog.addMessage("User " + email + " has registered an account");
            checkAndCreateFolder();
        } else {
            serverlog.addMessage("User " + email + " has failed to register an account");
        }
        sendResult(registerSuccess);
    }

    /**
     * Read the email from the client.
     * Try to reset password for the account with given email address (contact database).
     * Send a boolean to the client to tell whether password reset was successful or not.
     */
    private void resetPassword() {
        try {
            email = socketIn.readUTF();
        } catch (IOException ex) {
            serverlog.addMessage("Connection with client failed");
        }
        boolean resetPasswordSuccess = PersistenceController.getInstance().resetPasswordFromUser(email);
        if (resetPasswordSuccess) {
            serverlog.addMessage("User " + email + " has reseted his password");
        } else {
            serverlog.addMessage("User " + email + " has failed to reset his password");
        }
        sendResult(resetPasswordSuccess);
    }

    /**
     * Checks if the user already has a personal folder on the server.
     * If he hasn't the server will create one.
     */
    private void checkAndCreateFolder() {
        File dir = new File(email);
        if (!dir.exists()) {
            dir.mkdir();
            serverlog.addMessage("Client " + email + " has created his rootfolder");
        }
    }

    /**
     * Sends a boolean to the user.
     * 
     * @param result The boolean that needs to be sent.
     */
    private void sendResult(boolean result) {
        try {
            socketOut.writeBoolean(result);
        } catch (IOException ex) {
            serverlog.addMessage("Connection with client failed");
        }
    }

    /**
     * Accepts a file from the client:
     * The file will be stored in the user his personal folder on the server.
     */
    private void acceptFileFromClient() {
        try {
            String relativePath = socketIn.readUTF(); // Receive file name 
            serverlog.addMessage(email + " is uploading " + relativePath);
            int fileSize = socketIn.readInt(); // Receive file size
            File f = null;
            if(relativePath.lastIndexOf("/") > -1) {
                String pathFromRootToFile = relativePath.substring(0, relativePath.lastIndexOf("/"));
                String fileName = relativePath.substring(relativePath.lastIndexOf("/")+1);
                f = new File(email + "\\" + pathFromRootToFile, fileName);
            }
            else {
                f = new File(email, relativePath);
            }
            File d = new File(f.getParent());
            d.mkdirs();
            DataOutputStream fileOut = new DataOutputStream(new FileOutputStream(f));

            int n, totalSize = 0;
            byte[] buffer = new byte[BUFFERSIZE];
            while (!(totalSize == fileSize)) {
                //System.gc();
                n = BUFFERSIZE;
                totalSize += n;
                if (totalSize > fileSize) {
                    n -= totalSize - fileSize;
                    totalSize = fileSize;
                }
                buffer = new byte[n];
                socketIn.readFully(buffer);
                fileOut.write(buffer);
                //fileOut.flush();
            }
            fileOut.flush();
            fileOut.close();
            serverlog.addMessage(email + " finished uploading " + relativePath);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Deletes a file on the server
     *
     * @param relativePath The relative path, from the personal folder of the user, to the file that needs to be deleted
     * @throws IOException If there are errors reading the file
     */
    private void deleteFileOnServer(String relativePath) throws IOException {
        File file = new File(email, relativePath);
        serverlog.addMessage(email + " deletes file: " + file.getName());
        if (file.exists()) {
            if (!file.canWrite()) {
                throw new IllegalArgumentException("Delete: write protected: " + file.getName());
            }
            Utils.deleteFile(file.toPath(), email);
        }
    }

    /**
     * Reads a file path from the client and deletes that file
     */
    private void deleteFileOnServer() {
        try {
            deleteFileOnServer(socketIn.readUTF()); // Receive file name
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Sends a file to the client
     *
     * @param relatiefPath The relative path, from the personal folder of the user, to the file that needs to be send
     */
    private void sendFileToClient(String relatiefPath) {
        try {
            File file = new File(email, relatiefPath);
            int fileSize = (int) file.length();

            serverlog.addMessage("Sending " + file.getName() + " to " + email);

            socketOut.writeUTF("acceptFile");
            socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath()); // Send file name
            socketOut.writeInt(fileSize); // Send file size

            int n;
            byte[] buffer = new byte[BUFFERSIZE];
            byte[] bufferS;
            DataInputStream fileIn = new DataInputStream(new FileInputStream(file));
            while ((n = fileIn.read(buffer)) > 0) {
                bufferS = new byte[n];
                System.arraycopy(buffer, 0, bufferS, 0, n);
                socketOut.write(bufferS, 0, n);
                //socketOut.flush();
            }
            fileIn.close();
            socketOut.flush();
        } catch (IOException ex) {
            Logger.getLogger(ClientConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Method used to perform the initial sync.
     */
    private void synchronizeFiles() {
        serverlog.addMessage(email + " synchronizing files...");

        dir = new File(this.email);
        List<File> fileList = Utils.getFileList(dir);

        for (File file : fileList) {
            try {
                socketOut.writeUTF("file");
                socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath());
                socketOut.writeLong(Files.getLastModifiedTime(file.toPath()).toMillis());
                socketOut.write(Utils.calculateFileHash(file));

                String fileStatus = socketIn.readUTF();

                if (fileStatus.equals("changed_client")) {
                    deleteFileOnServer(dir.toURI().relativize(file.toURI()).getPath());
                    if (socketIn.readUTF().equals("acceptFile")) {
                        acceptFileFromClient();
                    }
                } else if (fileStatus.equals("changed_server")) {
                    sendFileToClient(dir.toURI().relativize(file.toURI()).getPath());
                } else if (fileStatus.equals("notfound")) {
                    sendFileToClient(dir.toURI().relativize(file.toURI()).getPath());
                }
            } catch (IOException e) {
                e.printStackTrace();
                serverlog.addMessage("Error synchronizing file: " + file.getName());
            }
        }

        try {
            socketOut.writeUTF("end");

            while (socketIn.readUTF().equals("acceptFile")) {
                acceptFileFromClient();
            }
        } catch (IOException e) {
            serverlog.addMessage("Error synchronizing client file.");
        }
        serverlog.addMessage(email + " finished synchronizing");
    }
    
}